package top.yangguangmc.sunshine_anticheat.utils;

import me.clip.placeholderapi.PlaceholderAPI;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.SecureRandom;
import java.util.*;

import static top.yangguangmc.sunshine_anticheat.utils.Builtin.ssac;

public class Utils {
    private static final Random random = new SecureRandom();
    private static String NMS_PACKAGE;
    private static Unsafe UNSAFE = null;

    static {
        String mcVersion = Bukkit.getVersion().substring(Bukkit.getVersion().indexOf("(MC:") + 5, Bukkit.getVersion().lastIndexOf(')'));
        String[] versions = mcVersion.split("\\.");
        String majorVer = versions[0];
        String minorVer = versions[1];
        String nmsBaseHead = "v" + majorVer + "_" + minorVer + "_R";
        for (int i = 1; i <= 9; i++) {
            String testVersion = nmsBaseHead + i;
            try {
                Class.forName("net.minecraft.server." + testVersion + ".MinecraftServer");
                NMS_PACKAGE = testVersion;
                break;
            } catch (ClassNotFoundException ignored) {
            }
        }
        try {
            for (Field field : Unsafe.class.getDeclaredFields()) {
                if (!field.getType().equals(Unsafe.class)) continue;
                if (!Modifier.isStatic(field.getModifiers())) continue;
                field.setAccessible(true);
                UNSAFE = (Unsafe) field.get(null);
                break;
            }
        } catch (IllegalAccessException ignored) {
        }
    }

    /**
     * Util class cannot have instance.
     */
    private Utils() {
    }

    /**
     * Generate a string with specific length and random characters.
     *
     * @param length   The length of the string.
     * @param safeMode If true, the string will only include characters between 0-9,a-z,A-Z and some basic symbols.
     *                 Else it will include any unicode character between 33 and 2048, which may make the string
     *                 illegal to be printed in chat.
     * @return The random-generated string.
     * @throws IllegalArgumentException If length is not positive.
     */
    public static String randomString(int length, boolean safeMode) {
        if (length <= 0) throw new IllegalArgumentException("Length should be positive.");
        if (safeMode) {
            String charSource = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&_+`-=,./(){}[];:'\"|\\";
            char[] result = new char[length];
            for (int i = 0; i < length; ++i) {
                result[i] = charSource.charAt(random.nextInt(charSource.length()));
            }
            return new String(result);
        } else {
            char[] result = new char[length];
            for (int i = 0; i < length; ++i) {
                result[i] = (char) randomInt(33, 2048);
            }
            return new String(result);
        }
    }

    /**
     * Generate a random integer between the parameter "min" and "max".
     *
     * @param min The min number of the integer.
     * @param max The max number of the integer.
     * @return The random-generated integer.
     * @throws IllegalArgumentException If min or max is not positive or min is bigger than max.
     */
    public static int randomInt(int min, int max) {
        if (min < 0 || max < 0 || min > max)
            throw new IllegalArgumentException("Min and max should be positive. Min cannot be bigger than max.");
        return min + random.nextInt(max - min);
    }

    /**
     * Generate a random double between the parameter "min" and "max".
     * Illegal arguments will lead to returning min value instead of throwing an exception.
     *
     * @param min The min number of the double.
     * @param max The max number of the double.
     * @return The random-generated double.
     */
    public static double randomDouble(double min, double max) {
        if (min == max || max - min <= 0.0) return min;
        else return min + (max - min) * Math.random();
    }

    /**
     * Generate a random-generated uniformly boolean value.
     * This is likely the same as: {@code RandomUtil.passByChance(0.5)}.
     *
     * @return A random-generated uniformly boolean value.
     */
    public static boolean randomBoolean() {
        return random.nextBoolean();
    }

    /**
     * Returns true at the specific chance.
     *
     * @param chance The specific chance. 0 means impossible and 1 means always pass.
     */
    public static boolean passByChance(double chance) {
        if (chance <= 0) return false;
        if (chance >= 1) return true;
        return Math.random() <= chance;
    }

    public static void schedule(Runnable runnable, int ticksLater) {
        if (ticksLater <= 0) {
            runnable.run();
            return;
        }
        new BukkitRunnable() {
            @Override
            public void run() {
                runnable.run();
            }
        }.runTaskLater(ssac, ticksLater);
    }

    public static void scheduleLoop(Runnable runnable, int ticksEvery) {
        new BukkitRunnable() {
            @Override
            public void run() {
                runnable.run();
            }
        }.runTaskTimer(ssac, 0, ticksEvery);
    }

    public static boolean intersectRayAndAABB(AxisAlignedBB box, Location rayPoint, Vector rayDirection) {
        Location minPoint = box.getMinPoint(), maxPoint = box.getMaxPoint();
        if (rayPoint.getX() > minPoint.getX() && rayPoint.getX() < maxPoint.getX() &&
                rayPoint.getY() > minPoint.getY() && rayPoint.getY() < maxPoint.getY() &&
                rayPoint.getZ() > minPoint.getZ() && rayPoint.getZ() < maxPoint.getZ()) return true;
        double[] dir = {rayDirection.getX(), rayDirection.getY(), rayDirection.getZ()};
        double[] ori = {rayPoint.getX(), rayPoint.getY(), rayPoint.getZ()};
        double[] minBound = {minPoint.getX(), minPoint.getY(), minPoint.getZ()};
        double[] maxBound = {maxPoint.getX(), maxPoint.getY(), maxPoint.getZ()};
        for (int axis = 0; axis < 3; axis++) {
            double t = 0.0;
            if (Math.abs(dir[axis]) > 0.000001) {
                if (dir[axis] > 0) {
                    t = (minBound[axis] - ori[axis]) / dir[axis];
                } else {
                    t = (maxBound[axis] - ori[axis]) / dir[axis];
                }
            }
            if (t > 0) {   // 射线和平面相交
                // 判断交点是否在面内
                final Vector vec = rayDirection.clone().multiply(t).add(rayPoint.toVector());
                double[] pt = {vec.getX(), vec.getY(), vec.getZ()};
                if (minBound[(axis + 1) % 3] < pt[(axis + 1) % 3] && pt[(axis + 1) % 3] < maxBound[(axis + 1) % 3] &&
                        minBound[(axis + 2) % 3] < pt[(axis + 2) % 3] && pt[(axis + 2) % 3] < maxBound[(axis + 2) % 3]) {
                    return true;
                }
            }
        }
        return false;
    }

    public static @NotNull String preprocessString(String s) {
        return preprocessString(s, null);
    }

    public static @NotNull String preprocessString(@Nullable String s, @Nullable OfflinePlayer player) {
        if (s == null) return "";
        if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
            s = PlaceholderAPI.setPlaceholders(player, s);
        }
        s = ChatColor.translateAlternateColorCodes('&', s);
        if (player != null) {
            s = s.replaceAll("(%player%)|(%player_name%)", player.getName());
        }
        return s;
    }

    /**
     * Gets all the fields of the object (including fields that were extended from the super classes).
     * This will not include <b>static</b> fields.
     *
     * @param o The specific object.
     * @return All the fields.
     */
    public static @NotNull @UnmodifiableView List<Field> getFields(@NotNull Object o) throws SecurityException {
        Objects.requireNonNull(o);
        List<Field> fieldList = new LinkedList<>();
        Class<?> clazz = o.getClass();
        while (clazz != null) {
            Field[] fields = clazz.getDeclaredFields();
            fieldList.addAll(0, Arrays.asList(fields));
            fieldList.removeIf(field -> Modifier.isStatic(field.getModifiers()));
            clazz = clazz.getSuperclass();
        }
        return Collections.unmodifiableList(fieldList);
    }

    /**
     * Gets the specific class in package {@code net.minecraft.server.vX_X_RX} using reflection.
     * Parameter {@code className} is the name of the class in the package.
     * For example, if you want to get {@code net.minecraft.server.vX_X_RX.ItemStack}, you should use:
     * <pre>{@code
     * Class clazz = Utils.getNMSClass("ItemStack");
     * }</pre>
     * For another example, if you want to get {@code net.minecraft.server.vX_X_RX.some_package.SomeClass},
     * you should use:
     * <pre>{@code
     * Class clazz2 = Utils.getNMSClass("some_package.SomeClass");
     * }</pre>
     * (Generally shouldn't happen)
     *
     * @param className The name of the specific class.
     * @return {@code java.lang.Class} object of the class (if exists).
     * @throws ClassNotFoundException If we cannot find the specific class.
     */
    public static @NotNull Class<?> getNMSClass(String className) throws ClassNotFoundException {
        if (NMS_PACKAGE == null)
            throw new ClassNotFoundException("Package net.minecraft.server.v1_X_RX not found.");
        return Class.forName("net.minecraft.server." + NMS_PACKAGE + "." + className);
    }

    /**
     * Gets the specific class in package {@code org.bukkit.craftbukkit.vX_X_RX} using reflection.
     *
     * @see #getNMSClass(String)
     */
    public static @NotNull Class<?> getCraftClass(String className) throws ClassNotFoundException {
        if (NMS_PACKAGE == null)
            throw new ClassNotFoundException("Package net.minecraft.server.v1_X_RX not found.");
        return Class.forName("org.bukkit.craftbukkit." + NMS_PACKAGE + "." + className);
    }

    public static Object unsafeShallowCopy(Object obj) throws Exception {
        if (obj == null) return null;
        Class<?> clazz = obj.getClass();
        Object newO = UNSAFE.allocateInstance(clazz);   //存在内存泄漏？
        for (Field field : getFields(obj)) {
            if (Modifier.isFinal(field.getModifiers())) continue;
            field.setAccessible(true);
            field.set(newO, field.get(obj));
        }
        return newO;
    }

    public static Object unsafeCloneEntity(Entity entity) throws Exception {
        Object oldEntity = getCraftClass("entity.CraftEntity").getDeclaredMethod("getHandle").invoke(entity);
        return unsafeShallowCopy(oldEntity);
    }

    public static ItemStack @NotNull [] getHotbar(@NotNull Player player) {
        ItemStack[] stacks = new ItemStack[9];
        for (int i = 0; i < 8; i++)
            stacks[i] = player.getInventory().getItem(i);
        return stacks;
    }
}
